package com.flmes.base.config;

import com.flmes.base.constant.RequestConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * @Author: flmes
 * @Date: 2024/10/8 19:19
 */
@Slf4j(topic = "api")
@ConditionalOnClass(Filter.class)
@ConditionalOnProperty(name = "flmes.http.log.enabled", havingValue = "true")
@Component
public class HttpLogFilter extends OncePerRequestFilter {

    private final PathMatcher pathMatcher = new AntPathMatcher();
    /**
     * 记录格式
     */
    private final String LOG_FORMATTER = "time:{0}ms, Request => {1} {2} {3}, Response <= {4}, {5}, ex:{6}";
    private List<String> excludes = new ArrayList<>();

    public HttpLogFilter() {
        // 不记录的请求
//        excludes.add("/user/login");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String uri = request.getRequestURI();
        // 未开启info级别日志或设置了不记录日志
        if ((!log.isInfoEnabled()) || excludes.stream().anyMatch(exclude -> pathMatcher.match(exclude, uri))) {
            filterChain.doFilter(request, response);
            return;
        }
        // 请求前埋点
        request.setAttribute(RequestConstant.START_TIME, System.currentTimeMillis());
        // 使用ContentCachingRequestWrapper跟ContentCachingResponseWrapper,保证request跟response内容可重复读
        if (!(request instanceof ContentCachingRequestWrapper)) {
            request = new ContentCachingRequestWrapper(request);
        }
        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
        }
        Exception resultEx = null;
        try {
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            resultEx = e;
            throw e;
        } finally {
            request.setAttribute(RequestConstant.END_TIME, System.currentTimeMillis());
            log.info("{}", logFormatter(request, response, resultEx));
            // 使用ContentCachingResponseWrapper必须再调用copyBodyToResponse,否则客户端可能读不了返回值
            ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
            Objects.requireNonNull(wrapper).copyBodyToResponse();
        }
    }

    /**
     * 组装记录内容
     *
     * @param request
     * @param response
     * @param resultEx
     * @return
     */
    public String logFormatter(HttpServletRequest request, HttpServletResponse response, Exception resultEx) {
        // 请求方法
        String method = request.getMethod();
        // URI
        String uri = request.getRequestURI();
        // 请求体
        byte[] requestContent = ((ContentCachingRequestWrapper) request).getContentAsByteArray();
        // 响应状态
        int status = response.getStatus();
        // 响应体
        byte[] responseContent = ((ContentCachingResponseWrapper) response).getContentAsByteArray();
        // 执行时间
        long time = (long) request.getAttribute(RequestConstant.END_TIME) - (long) request.getAttribute(RequestConstant.START_TIME);
        return MessageFormat.format(LOG_FORMATTER, time, method, uri, new String(requestContent), status, new String(responseContent), resultEx);
    }

}
